<?php

namespace Pinenut\Model2\Adapter;

use Pinenut\Model2\Finder;
use Pinenut\Model2\Updater;

class Mongo extends \Pinenut\Model2\Adapter {
	static $config = '\MongoAdapterConfig';
	
	/**
	 *
	 * @return \MongoId
	 */
	static function generateId() {
		return new \MongoId ();
	}
	
	/**
	 * Соединения с разными коллекциями
	 *
	 * @var \MongoCollection
	 */
	protected $connect = null;
	
	/**
	 * Коннект к коллекции базы
	 *
	 * @return \MongoCollection
	 */
	function connect() {
		if (null === $this->connect) {
			$configClassName = static::$config;
			$config = $configClassName::get ();
			$client = new \MongoClient ( @$config ['server'], @$config ['options'] );
			$db = $client->selectDB ( @$config ['db'] );
			$this->connect = $db->selectCollection ( $this->collection );
			ini_set('mongo.native_long', 1); // включаем поддержку длинных целых принудительно
			// подразумеваем, что всегда работаем с 64bit операционкой
		}
		return $this->connect;
	}
	protected function doCriteria(array $where) {
		$query = array ();
		
		foreach ( $where as $ops ) {
			foreach ( $ops as $op => $row ) {
				
				// не красиво, но для быстроты - уходим с отсутствием типизации идентификатора
				if ('_id' == $row ['field']) {
					if (is_array ( $row ['value'] )) {
						foreach ( $row ['value'] as &$value ) {
							try {
								$value = new \MongoId ( ( string ) $value );
							} catch ( \Exception $e ) {
								$value = null;
							}
						}
					} else {
						try {	
							$row ['value'] = new \MongoId ( ( string ) $row ['value'] );
						} catch ( \Exception $e ) {
							$row ['value'] = null;
						}
					}
				}
				
				// формируем запрос в зависимости от операции
				
				switch ($row ['op']) {
					case Finder::OP_EQ :
						$query [$row ['field']] = $row ['value'];
						break;
					case Finder::OP_NEQ :
						$query [$row ['field']] ['$ne'] = $row ['value'];
						break;
					case Finder::OP_IN :
						$query [$row ['field']] ['$in'] = $row ['value'];
						break;
					case Finder::OP_ALL :
						$query [$row ['field']] ['$all'] = $row ['value'];
						break;
					case Finder::OP_NIN :
						$query [$row ['field']] ['$nin'] = $row ['value'];
						break;
					
					case Finder::OP_GT :
						$query [$row ['field']] ['$gt'] = $row ['value'];
						break;
					
					case Finder::OP_LT :
						$query [$row ['field']] ['$lt'] = $row ['value'];
						break;
					
					case Finder::OP_GTE :
						$query [$row ['field']] ['$gte'] = $row ['value'];
						break;
					
					case Finder::OP_LTE :
						$query [$row ['field']] ['$lte'] = $row ['value'];
						break;
				}
			}
		}

		return $query;
	}
	
	/**
	 * Собираем и выполняем запрос на выборку данных
	 *
	 * @param array $where        	
	 * @param array $fields        	
	 * @return array
	 */
	function select(array $where = null, array $fields = null, array $order = null, $offset = null, $limit = null) {

		\Log::begin(null, 'Mongo');
		
		$query = $this->doCriteria ( $where );

		if (!is_array($fields)) {
			$fields = [];
		} 
		
		$cleanFields = array_values($fields);
		
		$result = $this->connect ()->find ( $query, $cleanFields );
		if (null !== $order)
			$result->sort ( $order );
		if (null !== $offset)
			$result->skip ( intval ( $offset ) );
		if (null !== $limit)
			$result->limit ( intval ( $limit ) );
		
		$data = iterator_to_array ( $result );

//echo "select ".$this->collection." ".implode(', ', array_keys($query)).' '.count($data)."\n";
//print_r($query); 
		$data = array_map(function($raw) use ($fields) {
				foreach ((array)$fields as $key => $value) {
					if (is_numeric($key)) {
						continue;
					}

					static::mapAlias($key, $value, $raw);
				}
				return $raw;
			}, $data);

		\Log::end('Select from ' . $this->collection, array('query'=>$query, 'fields'=>$fields, 'count'=>count($data)));
		return $data;
	}
	
	/**
	 * Возвращаем кол-во записей по условию
	 *
	 * @param array $where        	
	 * @return int
	 */
	function count(array $where = null) {
		
		\Log::begin(null, 'Mongo');
		
		$query = $this->doCriteria ( $where );
		$result = $this->connect ()->count ( $query );
		
		\Log::end('Count from ' . $this->collection, array('query'=>$query, 'count'=>$result));
		
		return $result;
	}
	
	/**
	 * Возвращаем кол-во записей по условию
	 *
	 * @param array $where        	
	 * @return int
	 */
	function remove(array $where = null) {
		\Log::begin(null, 'Mongo');
		
		$query = $this->doCriteria ((array) $where );
		$result = $this->connect ()->remove ( $query );

		\Log::end('Remove from ' . $this->collection, array('query'=>$query, ));
		
		return $result;
	}
	
	/**
	 * Обновляем запись
	 *
	 * @param array $where        	
	 * @param array $operators        	
	 */
	function update(array $where = null, array $operators = null, $upsert = false) {
		
		\Log::begin (null, 'Mongo');
		
		// подготавливаем условие
		$query = $this->doCriteria ( $where );
		$removeFromSet = $addToSet = $inc = $set = array ();
		
		// готовим операции
		
		if (isset ( $operators [Updater::OP_REMOVEFROMSET] ) && is_array ( $operators [Updater::OP_REMOVEFROMSET] )) {
			$removeFromSet = $operators [Updater::OP_REMOVEFROMSET];
		}
		if (isset ( $operators [Updater::OP_ADDTOSET] ) && is_array ( $operators [Updater::OP_ADDTOSET] )) {
			$addToSet = $operators [Updater::OP_ADDTOSET];
		}
		
		if (isset ( $operators [Updater::OP_INC] ) && is_array ( $operators [Updater::OP_INC] ))
			foreach ( $operators [Updater::OP_INC] as $field => $data ) {
				$inc [$field] = $data;
			}
		
		if (isset ( $operators [Updater::OP_SET] ) && is_array ( $operators [Updater::OP_SET] ))
			foreach ( $operators [Updater::OP_SET] as $field => $data ) {
				$set [$field] = $data;
			}
		
		$op = array ();
		$options = array (
				'multiple' => true 
		);
		
		$options ['upsert'] = $upsert;
		
		if (! empty ( $removeFromSet ))
			$op ['$pullAll'] = $removeFromSet;
		
		if (! empty ( $addToSet ))
			$op ['$addToSet'] = $addToSet;
		
		if (! empty ( $inc ))
			$op ['$inc'] = $inc;
		
		if (! empty ( $set ))
			$op ['$set'] = $set;

		try {
			$res = $this->connect ()->update ( $query, $op, $options );
		} catch ( Exception $e ) {
			throw $e;
		}
		
		\Log::end ( 'Update ' . $this->collection, array (
				'query' => $query,
				'op' => $op 
		) );
		
//echo "update ".$this->collection." ". implode(', ', array_keys($op)) . "\n";		
	}

	/**
	 * Добавляем новую запись
	 *
	 * @param array $set
	 */
	function insert(array $set = null) {
		\Log::begin(null, 'Mongo');
		// готовим массив данных из сета
		$data = static::set2array ( $set );

		if (isset($data['_id']) && !($data['_id'] instanceof \MongoId)) {
			try {
				$data['_id'] = new \MongoId($data['_id']);
			} catch (\MongoException $e) {
				$data['_id'] = null;
			}
		}

		$res = $this->connect ()->insert ( $data );
		\Log::end('Insert to ' . $this->collection, array('data'=>$data,));
		
//echo "insert ".$this->collection . "\n";
	}
	static function set2array($set) {
		$data = [ ];
		foreach ( $set as $path => $value ) {
			$parts = explode ( '.', $path );
			foreach ( $parts as &$part ) {
				if (intval ( $part ) > 0) {
					$part = intval ( $part ) - 1; // FIXME: поправка, блин, вносится при создании массива в связях например... не красиво
				}
			}
			switch (count ( $parts )) {
				case 1 :
					$data [$parts [0]] = $value;
					break;
				case 2 :
					$data [$parts [0]] [$parts [1]] = $value;
					break;
				case 3 :
					$data [$parts [0]] [$parts [1]] [$parts [2]] = $value;
					break;
				case 4 :
					$data [$parts [0]] [$parts [1]] [$parts [2]] [$parts [3]] = $value;
					break;
				case 5 :
					$data [$parts [0]] [$parts [1]] [$parts [2]] [$parts [3]] [$parts [4]] = $value;
					break;
			}
		}
		return $data;
	}
	function ensureIndex(array $fields, $name = null, $params = array()) {
		if (null !== $name) {
			$params ['name'] = $name;
		}
		return $this->connect ()->ensureIndex ( $fields, $params );
	}
	
	/*
	 * вызываем функцию aggregate с параметрами. Возвращает массив.
	 */
	function aggregate($where,$fields,$groupFields,$ops) {
		\Log::begin(null, 'Mongo');
		
		$group = $ops;
		if(count($groupFields) == 1){
			$group['_id'] = current($groupFields);
		}else{
			$group['_id'] = $groupFields;
		}
		
		$criteria = $this->doCriteria($where);
		
		if ($criteria) {
			$pipeline[] = array('$match'=>$criteria);
		}
		$pipeline[] = array('$project'=>$fields);
		$pipeline[] = array('$group'=>$group);
		
		return $this->connect ()->aggregate ( $pipeline );
		\Log::end ( 'Aggregate ' . $this->collection, array (
				'pipeline' => $pipeline,
		) );
		
	}

	protected static function mapAlias($alias, $field, &$raw)
	{
		$offset = strpos($field, '.');
		if ($offset === false) {
			$raw[$alias] = $raw[$field];
			unset($raw[$field]);
			return;
		}

		$parts = explode('.', $field);
		$link = &$raw;
		foreach ($parts as $part) {
			if (!isset($link[$part])) {
				$link = null;
				break;
				// имхо лишний, надо просто нул складывать
				//throw new \Exception('Wrong field path (' . $field . ').');
			}
			$link = &$link[$part];
		}
		$raw[$alias] = $link;
		unset($link);
	}
}